/*
 * @Author: wangshuixin 2412658453@qq.com
 * @Date: 2024-12-07 14:07:37
 * @LastEditors: wangshuixin 2412658453@qq.com
 * @LastEditTime: 2024-12-12 13:01:21
 * @FilePath: \cpp-cmake\src\main.cpp
 * @Description: 程序入口
 */
#if defined(_MSC_VER)
// Make MS math.h define M_PI
#define _USE_MATH_DEFINES
#endif

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define GLAD_GL_IMPLEMENTATION
#include <glad/gl.h>
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

#include <linmath.h>

/*****************************************************************************
 * 各种声明 和 宏定义
 *****************************************************************************/

/* 原型 */
void init(void);
void display(void);
void reshape(GLFWwindow *window, int w, int h);
void key_callback(GLFWwindow *window, int key, int scancode, int action, int mods);
void mouse_button_callback(GLFWwindow *window, int button, int action, int mods);
void cursor_position_callback(GLFWwindow *window, double x, double y);
void DrawBoingBall(void);
void BounceBall(double dt);
void DrawBoingBallBand(GLfloat long_lo, GLfloat long_hi);
void DrawGrid(void);

/* 宏定义 */
#define RADIUS 70.f
#define STEP_LONGITUDE 22.5f /* 22.5 makes 8 bands like original Boing */
#define STEP_LATITUDE 22.5f

#define DIST_BALL (RADIUS * 2.f + RADIUS * 0.1f)

#define VIEW_SCENE_DIST (DIST_BALL * 3.f + 200.f) /* distance from viewer to middle of boing area */
#define GRID_SIZE (RADIUS * 4.5f)                 /* length (width) of grid */
#define BOUNCE_HEIGHT (RADIUS * 2.1f)
#define BOUNCE_WIDTH (RADIUS * 2.1f)

#define SHADOW_OFFSET_X -20.f
#define SHADOW_OFFSET_Y 10.f
#define SHADOW_OFFSET_Z 0.f

#define WALL_L_OFFSET 0.f
#define WALL_R_OFFSET 5.f

/* 动画速度 (50.0f 模仿原始的GLUT demo的速度) */
#define ANIMATION_SPEED 50.f

/* 每次物理迭代允许的最大增量时间 */
#define MAX_DELTA_T 0.02f

/* 画球或它的影子 */
typedef enum
{
   DRAW_BALL,
   DRAW_BALL_SHADOW
} DRAW_BALL_ENUM;

/* 节点类型 */
typedef struct
{
   float x;
   float y;
   float z;
} vertex_t;

/* Global vars */
int windowed_xpos, windowed_ypos, windowed_width, windowed_height;
int width, height;
GLfloat deg_rot_y = 0.f;
GLfloat deg_rot_y_inc = 2.f;
int override_pos = GLFW_FALSE;
GLfloat cursor_x = 0.f;
GLfloat cursor_y = 0.f;
GLfloat ball_x = -RADIUS;
GLfloat ball_y = -RADIUS;
GLfloat ball_x_inc = 1.f;
GLfloat ball_y_inc = 2.f;
DRAW_BALL_ENUM drawBallHow;
double t;
double t_old = 0.f;
double dt;

/* Random number generator */
#ifndef RAND_MAX
#define RAND_MAX 4095
#endif

/*****************************************************************************
 * Truncate a degree.
 *****************************************************************************/
GLfloat TruncateDeg(GLfloat deg)
{
   if (deg >= 360.f)
      return (deg - 360.f);
   else
      return deg;
}

/*****************************************************************************
 * Convert a degree (360-based) into a radian.
 * 360' = 2 * PI
 *****************************************************************************/
double deg2rad(double deg)
{
   return deg / 360 * (2 * M_PI);
}

/*****************************************************************************
 * 360' sin().
 *****************************************************************************/
double sin_deg(double deg)
{
   return sin(deg2rad(deg));
}

/*****************************************************************************
 * 360' cos().
 *****************************************************************************/
double cos_deg(double deg)
{
   return cos(deg2rad(deg));
}

/*****************************************************************************
 * Compute a cross product (for a normal vector).
 *
 * c = a x b
 *****************************************************************************/
void CrossProduct(vertex_t a, vertex_t b, vertex_t c, vertex_t *n)
{
   GLfloat u1, u2, u3;
   GLfloat v1, v2, v3;

   u1 = b.x - a.x;
   u2 = b.y - a.y;
   u3 = b.y - a.z;

   v1 = c.x - a.x;
   v2 = c.y - a.y;
   v3 = c.z - a.z;

   n->x = u2 * v3 - v2 * u3;
   n->y = u3 * v1 - v3 * u1;
   n->z = u1 * v2 - v1 * u2;
}

#define BOING_DEBUG 0

/*****************************************************************************
 * init()
 *****************************************************************************/
void init(void)
{
   /*
    * Clear background.
    */
   // 渲染指令
   glClearColor(0.55f, 0.55f, 0.55f, 0.f); // 设置背景色
   glClear(GL_COLOR_BUFFER_BIT);           // 清除颜色缓冲

   glShadeModel(GL_FLAT);
}

/*****************************************************************************
 * display()
 *****************************************************************************/
void display(void)
{
   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
   glPushMatrix();

   drawBallHow = DRAW_BALL_SHADOW;
   DrawBoingBall();

   DrawGrid();

   drawBallHow = DRAW_BALL;
   DrawBoingBall();

   glPopMatrix();
   glFlush();
}

/*****************************************************************************
 * reshape()
 *****************************************************************************/
void reshape(GLFWwindow *window, int w, int h)
{
   mat4x4 projection, view;

   // 更新OpenGL视口大小
   glViewport(0, 0, (GLsizei)w, (GLsizei)h);

   glMatrixMode(GL_PROJECTION);
   mat4x4_perspective(projection,
                      2.f * (float)atan2(RADIUS, 200.f),
                      (float)w / (float)h,
                      1.f, VIEW_SCENE_DIST);
   glLoadMatrixf((const GLfloat *)projection);

   glMatrixMode(GL_MODELVIEW);
   {
      vec3 eye = {0.f, 0.f, VIEW_SCENE_DIST};
      vec3 center = {0.f, 0.f, 0.f};
      vec3 up = {0.f, -1.f, 0.f};
      mat4x4_look_at(view, eye, center, up);
   }
   glLoadMatrixf((const GLfloat *)view);
}

void key_callback(GLFWwindow *window, int key, int scancode, int action, int mods)
{
   if (action != GLFW_PRESS)
      return;

   if (key == GLFW_KEY_ESCAPE && mods == 0)
      glfwSetWindowShouldClose(window, GLFW_TRUE);
   if ((key == GLFW_KEY_ENTER && mods == GLFW_MOD_ALT) ||
       (key == GLFW_KEY_F11 && mods == GLFW_MOD_ALT))
   {
      if (glfwGetWindowMonitor(window))
      {
         glfwSetWindowMonitor(window, NULL,
                              windowed_xpos, windowed_ypos,
                              windowed_width, windowed_height, 0);
      }
      else
      {
         GLFWmonitor *monitor = glfwGetPrimaryMonitor();
         if (monitor)
         {
            const GLFWvidmode *mode = glfwGetVideoMode(monitor);
            glfwGetWindowPos(window, &windowed_xpos, &windowed_ypos);
            glfwGetWindowSize(window, &windowed_width, &windowed_height);
            glfwSetWindowMonitor(window, monitor, 0, 0, mode->width, mode->height, mode->refreshRate);
         }
      }
   }
}

static void set_ball_pos(GLfloat x, GLfloat y)
{
   ball_x = (width / 2) - x;
   ball_y = y - (height / 2);
}

void mouse_button_callback(GLFWwindow *window, int button, int action, int mods)
{
   if (button != GLFW_MOUSE_BUTTON_LEFT)
      return;

   if (action == GLFW_PRESS)
   {
      override_pos = GLFW_TRUE;
      set_ball_pos(cursor_x, cursor_y);
   }
   else
   {
      override_pos = GLFW_FALSE;
   }
}

void cursor_position_callback(GLFWwindow *window, double x, double y)
{
   cursor_x = (float)x;
   cursor_y = (float)y;

   if (override_pos)
      set_ball_pos(cursor_x, cursor_y);
}

/*****************************************************************************
 * Draw the Boing ball.
 *
 * The Boing ball is sphere in which each facet is a rectangle.
 * Facet colors alternate between red and white.
 * The ball is built by stacking latitudinal circles.  Each circle is composed
 * of a widely-separated set of points, so that each facet is noticeably large.
 *****************************************************************************/
void DrawBoingBall(void)
{
   GLfloat lon_deg; /* degree of longitude */
   double dt_total, dt2;

   glPushMatrix();
   glMatrixMode(GL_MODELVIEW);

   /*
    * Another relative Z translation to separate objects.
    */
   glTranslatef(0.0, 0.0, DIST_BALL);

   /* Update ball position and rotation (iterate if necessary) */
   dt_total = dt;
   while (dt_total > 0.0)
   {
      dt2 = dt_total > MAX_DELTA_T ? MAX_DELTA_T : dt_total;
      dt_total -= dt2;
      BounceBall(dt2);
      deg_rot_y = TruncateDeg(deg_rot_y + deg_rot_y_inc * ((float)dt2 * ANIMATION_SPEED));
   }

   /* Set ball position */
   glTranslatef(ball_x, ball_y, 0.0);

   /*
    * Offset the shadow.
    */
   if (drawBallHow == DRAW_BALL_SHADOW)
   {
      glTranslatef(SHADOW_OFFSET_X,
                   SHADOW_OFFSET_Y,
                   SHADOW_OFFSET_Z);
   }

   /*
    * Tilt the ball.
    */
   glRotatef(-20.0, 0.0, 0.0, 1.0);

   /*
    * Continually rotate ball around Y axis.
    */
   glRotatef(deg_rot_y, 0.0, 1.0, 0.0);

   /*
    * Set OpenGL state for Boing ball.
    */
   glCullFace(GL_FRONT);
   glEnable(GL_CULL_FACE);
   glEnable(GL_NORMALIZE);

   /*
    * Build a faceted latitude slice of the Boing ball,
    * stepping same-sized vertical bands of the sphere.
    */
   for (lon_deg = 0;
        lon_deg < 180;
        lon_deg += STEP_LONGITUDE)
   {
      /*
       * Draw a latitude circle at this longitude.
       */
      DrawBoingBallBand(lon_deg,
                        lon_deg + STEP_LONGITUDE);
   }

   glPopMatrix();

   return;
}

/*****************************************************************************
 * Bounce the ball.
 *****************************************************************************/
void BounceBall(double delta_t)
{
   GLfloat sign;
   GLfloat deg;

   if (override_pos)
      return;

   /* Bounce on walls */
   if (ball_x > (BOUNCE_WIDTH / 2 + WALL_R_OFFSET))
   {
      ball_x_inc = -0.5f - 0.75f * (GLfloat)rand() / (GLfloat)RAND_MAX;
      deg_rot_y_inc = -deg_rot_y_inc;
   }
   if (ball_x < -(BOUNCE_HEIGHT / 2 + WALL_L_OFFSET))
   {
      ball_x_inc = 0.5f + 0.75f * (GLfloat)rand() / (GLfloat)RAND_MAX;
      deg_rot_y_inc = -deg_rot_y_inc;
   }

   /* Bounce on floor / roof */
   if (ball_y > BOUNCE_HEIGHT / 2)
   {
      ball_y_inc = -0.75f - 1.f * (GLfloat)rand() / (GLfloat)RAND_MAX;
   }
   if (ball_y < -BOUNCE_HEIGHT / 2 * 0.85)
   {
      ball_y_inc = 0.75f + 1.f * (GLfloat)rand() / (GLfloat)RAND_MAX;
   }

   /* Update ball position */
   ball_x += ball_x_inc * ((float)delta_t * ANIMATION_SPEED);
   ball_y += ball_y_inc * ((float)delta_t * ANIMATION_SPEED);

   /*
    * Simulate the effects of gravity on Y movement.
    */
   if (ball_y_inc < 0)
      sign = -1.0;
   else
      sign = 1.0;

   deg = (ball_y + BOUNCE_HEIGHT / 2) * 90 / BOUNCE_HEIGHT;
   if (deg > 80)
      deg = 80;
   if (deg < 10)
      deg = 10;

   ball_y_inc = sign * 4.f * (float)sin_deg(deg);
}

/*****************************************************************************
 * Draw a faceted latitude band of the Boing ball.
 *
 * Parms:   long_lo, long_hi
 *          Low and high longitudes of slice, resp.
 *****************************************************************************/
void DrawBoingBallBand(GLfloat long_lo,
                       GLfloat long_hi)
{
   vertex_t vert_ne; /* "ne" means south-east, so on */
   vertex_t vert_nw;
   vertex_t vert_sw;
   vertex_t vert_se;
   vertex_t vert_norm;
   GLfloat lat_deg;
   static int colorToggle = 0;

   /*
    * Iterate through the points of a latitude circle.
    * A latitude circle is a 2D set of X,Z points.
    */
   for (lat_deg = 0;
        lat_deg <= (360 - STEP_LATITUDE);
        lat_deg += STEP_LATITUDE)
   {
      /*
       * Color this polygon with red or white.
       */
      if (colorToggle)
         glColor3f(0.8f, 0.1f, 0.1f);
      else
         glColor3f(0.95f, 0.95f, 0.95f);
#if 0
      if ( lat_deg >= 180 )
         if ( colorToggle )
            glColor3f( 0.1f, 0.8f, 0.1f );
         else
            glColor3f( 0.5f, 0.5f, 0.95f );
#endif
      colorToggle = !colorToggle;

      /*
       * Change color if drawing shadow.
       */
      if (drawBallHow == DRAW_BALL_SHADOW)
         glColor3f(0.35f, 0.35f, 0.35f);

      /*
       * Assign each Y.
       */
      vert_ne.y = vert_nw.y = (float)cos_deg(long_hi) * RADIUS;
      vert_sw.y = vert_se.y = (float)cos_deg(long_lo) * RADIUS;

      /*
       * Assign each X,Z with sin,cos values scaled by latitude radius indexed by longitude.
       * Eg, long=0 and long=180 are at the poles, so zero scale is sin(longitude),
       * while long=90 (sin(90)=1) is at equator.
       */
      vert_ne.x = (float)cos_deg(lat_deg) * (RADIUS * (float)sin_deg(long_lo + STEP_LONGITUDE));
      vert_se.x = (float)cos_deg(lat_deg) * (RADIUS * (float)sin_deg(long_lo));
      vert_nw.x = (float)cos_deg(lat_deg + STEP_LATITUDE) * (RADIUS * (float)sin_deg(long_lo + STEP_LONGITUDE));
      vert_sw.x = (float)cos_deg(lat_deg + STEP_LATITUDE) * (RADIUS * (float)sin_deg(long_lo));

      vert_ne.z = (float)sin_deg(lat_deg) * (RADIUS * (float)sin_deg(long_lo + STEP_LONGITUDE));
      vert_se.z = (float)sin_deg(lat_deg) * (RADIUS * (float)sin_deg(long_lo));
      vert_nw.z = (float)sin_deg(lat_deg + STEP_LATITUDE) * (RADIUS * (float)sin_deg(long_lo + STEP_LONGITUDE));
      vert_sw.z = (float)sin_deg(lat_deg + STEP_LATITUDE) * (RADIUS * (float)sin_deg(long_lo));

      /*
       * Draw the facet.
       */
      glBegin(GL_POLYGON);

      CrossProduct(vert_ne, vert_nw, vert_sw, &vert_norm);
      glNormal3f(vert_norm.x, vert_norm.y, vert_norm.z);

      glVertex3f(vert_ne.x, vert_ne.y, vert_ne.z);
      glVertex3f(vert_nw.x, vert_nw.y, vert_nw.z);
      glVertex3f(vert_sw.x, vert_sw.y, vert_sw.z);
      glVertex3f(vert_se.x, vert_se.y, vert_se.z);

      glEnd();

#if BOING_DEBUG
      printf("----------------------------------------------------------- \n");
      printf("lat = %f  long_lo = %f  long_hi = %f \n", lat_deg, long_lo, long_hi);
      printf("vert_ne  x = %.8f  y = %.8f  z = %.8f \n", vert_ne.x, vert_ne.y, vert_ne.z);
      printf("vert_nw  x = %.8f  y = %.8f  z = %.8f \n", vert_nw.x, vert_nw.y, vert_nw.z);
      printf("vert_se  x = %.8f  y = %.8f  z = %.8f \n", vert_se.x, vert_se.y, vert_se.z);
      printf("vert_sw  x = %.8f  y = %.8f  z = %.8f \n", vert_sw.x, vert_sw.y, vert_sw.z);
#endif
   }

   /*
    * Toggle color so that next band will opposite red/white colors than this one.
    */
   colorToggle = !colorToggle;

   /*
    * This circular band is done.
    */
   return;
}

/*****************************************************************************
 * Draw the purple grid of lines, behind the Boing ball.
 * When the Workbench is dropped to the bottom, Boing shows 12 rows.
 *****************************************************************************/
void DrawGrid(void)
{
   int row, col;
   const int rowTotal = 12;       /* must be divisible by 2 */
   const int colTotal = rowTotal; /* must be same as rowTotal */
   const GLfloat widthLine = 2.0; /* should be divisible by 2 */
   const GLfloat sizeCell = GRID_SIZE / rowTotal;
   const GLfloat z_offset = -40.0;
   GLfloat xl, xr;
   GLfloat yt, yb;

   glPushMatrix();
   glDisable(GL_CULL_FACE);

   /*
    * Another relative Z translation to separate objects.
    */
   glTranslatef(0.0, 0.0, DIST_BALL);

   /*
    * Draw vertical lines (as skinny 3D rectangles).
    */
   for (col = 0; col <= colTotal; col++)
   {
      /*
       * Compute co-ords of line.
       */
      xl = -GRID_SIZE / 2 + col * sizeCell;
      xr = xl + widthLine;

      yt = GRID_SIZE / 2;
      yb = -GRID_SIZE / 2 - widthLine;

      glBegin(GL_POLYGON);

      glColor3f(0.6f, 0.1f, 0.6f); /* purple */

      glVertex3f(xr, yt, z_offset); /* NE */
      glVertex3f(xl, yt, z_offset); /* NW */
      glVertex3f(xl, yb, z_offset); /* SW */
      glVertex3f(xr, yb, z_offset); /* SE */

      glEnd();
   }

   /*
    * Draw horizontal lines (as skinny 3D rectangles).
    */
   for (row = 0; row <= rowTotal; row++)
   {
      /*
       * Compute co-ords of line.
       */
      yt = GRID_SIZE / 2 - row * sizeCell;
      yb = yt - widthLine;

      xl = -GRID_SIZE / 2;
      xr = GRID_SIZE / 2 + widthLine;

      glBegin(GL_POLYGON);

      glColor3f(0.6f, 0.1f, 0.6f); /* purple */

      glVertex3f(xr, yt, z_offset); /* NE */
      glVertex3f(xl, yt, z_offset); /* NW */
      glVertex3f(xl, yb, z_offset); /* SW */
      glVertex3f(xr, yb, z_offset); /* SE */

      glEnd();
   }

   glPopMatrix();

   return;
}

/*======================================================================*
 * main() 程序加载入口
 *======================================================================*/

int main(void)
{
   GLFWwindow *window;

   /* 初始化GLFW */
   if (!glfwInit())
      exit(EXIT_FAILURE);

   // 创建一个窗口，并允许用户调整其大小
   window = glfwCreateWindow(400, 400, "Boing (classic Amiga demo)", NULL, NULL);
   if (!window)
   {
      // 终止GLFW
      glfwTerminate();
      exit(EXIT_FAILURE);
   }

   glfwSetWindowAspectRatio(window, 1, 1);

   // 设置窗口大小回调函数
   glfwSetFramebufferSizeCallback(window, reshape);
   glfwSetKeyCallback(window, key_callback);
   glfwSetMouseButtonCallback(window, mouse_button_callback);
   glfwSetCursorPosCallback(window, cursor_position_callback);

   // 设置当前OpenGL上下文为创建的窗口
   glfwMakeContextCurrent(window);
   gladLoadGL(glfwGetProcAddress);
   glfwSwapInterval(1);

   // 获取当前窗口的大小
   glfwGetFramebufferSize(window, &width, &height);
   reshape(window, width, height);

   glfwSetTime(0.0);

   init();

   /* Main loop */
   // 进入主循环，直到窗口关闭
   while (!glfwWindowShouldClose(window))
   {
      /* Timing */
      t = glfwGetTime();
      dt = t - t_old;
      t_old = t;

      /* Draw one frame */
      display();

      // 交换缓冲并查询IO事件
      glfwSwapBuffers(window);
      // 处理事件，如键盘输入、鼠标移动等
      glfwPollEvents();

      /* Check if we are still running */
      if (glfwWindowShouldClose(window))
         break;
   }
   // 清理资源
   // 销毁窗口
   glfwDestroyWindow(window);
   // 终止GLFW
   glfwTerminate();
   exit(EXIT_SUCCESS);
}